第5章定时器与数码管基础
随着学习内容越来越多,个别细节可能会感觉略有吃力。但是不要担心,这个跟小孩学走路一样,刚开始走得不太稳,没关系,多走几步多练练。看教材的时候要注意专心,一遍看不懂,思考一下,再回头看第二遍和第三遍,没准一下就明白了。如果三遍还看不明白,那就把不懂的问题放一放,继续往下学两课然后再回头看一次,也可以与他人讨论一下,可能就会茅塞顿开了。
5.1逻辑电路与逻辑运算
B站在线视频教程:5-1 逻辑电路与逻辑运算
在数字电路经常会遇到逻辑电路,而在C语言中则经常用到逻辑运算。二者在原理上是相互关联的,在这里就先简单介绍一下。
首先,在“逻辑”这个概念范畴内,存在真和假这两个逻辑值,而将其对应到数字电路或C语言中,就变成了“非0值”和“0值”这两个值,即逻辑上的“假”就是数字电路或C语言中的“0”这个值,而逻辑“真”就是其它一切“非0值”。
来具体学习一下几个主要的逻辑运算符。假定有2个字节变量:A和B,二者进行某种逻辑运算后的结果为F。
以下逻辑运算符都是按照变量整体值进行运算的,通常就叫做逻辑运算符:
&& 逻辑与。F = A && B,当A、B的值都为真(即非0值,下同)时,其运算结果F为真(具体数值为1,下同);当A、B值任意一个为假(即0,下同)时,结果F为假(具体数值为0,下同)。
|| 逻辑或。F = A || B,当A、B值任意一个为真时,其运算结果F为真;当A、B值都为假时,结果F为假。
! 逻辑非,F = !A,当A值为假时,其运算结果F为真;当A值为真时,结果F为假。
以下逻辑运算符都是按照变量内的每一个位来进行运算的,通常就叫做位运算符:
& 按位与,F = A & B,将A、B两个字节中的每一位都进行与运算,再将得到的每一位结果组合为总结果F,例如A = 0b11001100,B = 0b11110000,则结果F就等于0b11000000。
| 按位或,F = A | B,将A、B两个字节中的每一位都进行或运算,再将得到的每一位结果组合为总结果F,例如A = 0b11001100,B = 0b11110000,则结果F就等于0b11111100。
~ 按位取反,F = ~A,将A字节内的每一位进行非运算(就是取反),再将得到的每一位结果组合为总结果F,例如A = 0b11001100,则结果F就等于0b00110011;这个运算符流水灯实验里已经用过了,现在再回头看一眼,是不是清楚多了。
^ 按位异或,异或的意思是,如果运算双方的值不同(即相异)则结果为真,双方值相同则结果为假。在C语言里没有按变量整体值进行的异或运算,所以仅以按位异或为例,F = A ^ B,A = 0b11001100,B = 0b11110000,则结果F就等于0b00111100。
今后看资料或芯片手册的时候,会经常遇到一些电路符号,图5-1所示就是数字电路中的常用符号,知道这些符号有利于理解器件的逻辑结构,尤其重点认识图5-1中的国外流行图形符号。在这里先简单看一下,日后遇到了可以到这里来查阅。
5.2定时器的学习
B站在线视频教程:5-2 定时器和计数器
定时器是单片机系统的一个重点,但并不是难点,需要完全理解并且熟练掌握。
5.2.1定时器的初步认识
- 时钟周期 时钟周期:时钟周期T是时序中最小的时间单位,具体计算的方法是 时钟周期 =1/时钟源频率 Kingst51单片机开发板上用的晶振是11.0592M,那么对于这个单片机系统来说,时钟周期=1/11059200秒。
- 机器周期 单片机完成一个操作的最短时间。机器周期主要针对汇编语言而言,在汇编语言下程序的每一条语句执行所使用的时间都是机器周期的整数倍,语句占用的时间是可以计算出来的,而C语言一条语句的时间是不确定的,受到诸多因素的影响。51单片机系列,在其标准架构下一个机器周期是12个时钟周期,也就是12/11059200秒。而一些增强型的51单片机,其速度都更快一些,有的1个机器周期等于4个时钟周期,有的1个机器周期就等于1个时钟周期,也就是说大体上其速度可以达到标准51架构的3倍或12倍。Kingst51单片机采用的是标准的51单片机,所以后面的章节如果遇到机器周期这个概念,全部是指12个时钟周期。
时钟周期和机器周期两个概念了解即可,下边就来讲讲重头戏,定时器和计数器。定时器和计数器是单片机内部的同一个模块,通过配置SFR(特殊功能寄存器)可以实现两种不同的功能。大多数情况下是使用定时器功能,因此主要来讲定时器功能,计数器功能可自学。
顾名思义,定时器就是用来进行定时的。定时器内部有一个寄存器,让它开始计数后,这个寄存器的值每经过一个机器周期就会自动加1,因此,可以把机器周期理解为定时器的计数周期。就像钟表每经过一秒,数字自动加1一样,定时器是每过一个机器周期的时间,也就是12/11059200秒,数字自动加1。还有一个特别注意的地方,就是钟表是加到60后,秒就自动变成0了,这种情况在单片机或计算机里称之为溢出。那定时器加到多少才会溢出呢?后面会讲到定时器有多种工作模式,分别使用不同的位宽(指使用多少个二进制位),假如是16位的定时器,也就是2个字节,最大值就是65535,那么加到65535后,再加1就算溢出,如果有其他位数的话,道理是一样的,对于51单片机来说,溢出后,这个值会直接变成0。从某一个初始值开始,经过确定的时间后溢出,这个过程就是定时的含义。
5.2.2定时器的寄存器
标准的51单片机内部有T0和T1这两个定时器,T就是Timer的缩写,现在很多51系列单片机还会增加额外的定时器,在这里先讲定时器0和1。对于单片机的每一个功能模块,都是由它的SFR,也就是特殊功能寄存器来控制。与定时器有关的特殊功能寄存器,有以下几个,不需要去记忆这些寄存器的名字和作用,只要大概知道就行,用的时候随时可以查手册,找到每个寄存器的名字和每个寄存器所起到的作用。
表5-1的寄存器是存储定时器的计数值的。TH0/TL0用于T0,TH1/TL1用于T1。
表5-1 定时器存储寄存器
名称 | 描述 | SFR地址 | 复位值 |
---|---|---|---|
TH0 | 定时器0高字节 | 0x8C | 0x00 |
TL0 | 定时器0低字节 | 0x8A | 0x00 |
TH1 | 定时器1高字节 | 0x8D | 0x00 |
TL1 | 定时器1低字节 | 0x8B | 0x00 |
表5-2是定时器控制寄存器TCON的位分配,表5-3是则是对每一位的具体含义的描述。
表5-2 TCON——定时器控制寄存器的位分配(地址0x88、可位寻址)
位 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
符号 | TF1 | TR1 | TF0 | TR0 | IE1 | IT1 | IE0 | IT0 |
复位值 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
表5-3 TCON——定时器控制寄存器的位描述
位 | 符号 | 描述 |
---|---|---|
7 | TF1 | 定时器1溢出标志。一旦定时器1发生溢出时硬件置1。清零有两种方式:软件清零,或者进入定时器中断时硬件清零。 |
6 | TR1 | 定时器1运行控制位。软件置位/清零来进行启动/停止定时器。 |
5 | TF0 | 定时器0溢出标志。一旦定时器0发生溢出时硬件置1。清零有两种方式:软件清零,或者进入定时器中断时硬件清零。 |
4 | TR0 | 定时器0运行控制位。软件置位/清零来进行启动/停止定时器。 |
3 | IE1 | 外部中断部分,与定时器无关,暂且不看。 |
2 | IT1 | 外部中断部分,与定时器无关,暂且不看。 |
1 | IE0 | 外部中断部分,与定时器无关,暂且不看。 |
0 | IT0 | 外部中断部分,与定时器无关,暂且不看。 |
请注意在表5-3中的描述中,只要写到硬件置1或者清0的,就是指一旦符合条件,单片机将自动完成的动作,只要写软件置1或者清0的,是指必须用程序去完成这个动作,后续遇到此类描述就不再另做说明了。
对于TCON这个SFR,其中有TF1、TR1、TF0、TR0这4位需要理解清楚,它们分别对应于T1和T0。以定时器1为例讲解,那么定时器0同理。先看TR1,当程序中写TR1 = 1以后,定时器值就会每经过一个机器周期自动加1,当程序中写TR1 = 0以后,定时器就会停止加1,其值会保持不变化。TF1,这个是一个标志位,他的作用是通知用户定时器溢出了。比如定时器设置成16位的模式,那么每经过一个机器周期,TL1加1一次,当TL1加到255后,再加1,TL1变成0,TH1会加1一次,如此一直加到TH1和TL1都是255(即TH1和TL1组成的16位整型数为65535)以后,再加1一次,就会溢出了,TH1和TL1同时都变为0,只要一溢出,TF1马上自动变成1,通知用户定时器溢出了,仅仅是提供给用户一个信号,让用户知道定时器溢出了,它不会对定时器是否继续运行产生任何影响。
本节开头就提到了定时器有多种工作模式,工作模式的选择就由TMOD来控制,TMOD的位分配和描述见表5-4到5-6所示,TMOD的位功能如表5-5所示。
表5-4 TMOD——定时器模式寄存器的位分配(地址0x89、不可位寻址)
位 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
符号 | GATE | C/T | M1 | M0 | GATE | C/T | M1 | M0 |
(T1) | (T1) | (T1) | (T1) | (T0) | (T0) | (T0) | (T0) | |
复位值 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
表5-5 TMOD——定时器模式寄存器的位描述
符号 | 描述 |
---|---|
T1/T0 | 在表5-4中,标T1的表示控制定时器1的位,标T0的表示控制定时器0的位。 |
GATE | 该位被置1时为门控位。仅当‘INTx’脚为高并且‘TRx’控制位被置1时使能定时器‘x’,定时器开始计时,当该位被清0时,只要‘TRx’位被置1,定时器x就使能开始计时,不受到单片机引脚‘INTx’外部信号的干扰,常用来测量外部信号脉冲宽度。这是定时器一个额外功能,本节课暂不介绍。 |
C/T | 定时器或计数器选择位。该位被清零时用作定时器功能(内部系统时钟),被置1用作计数器功能。 |
表5-6 TMOD——定时器模式寄存器M1/M0工作模式
M1 | M0 | 工作模式 | 描述 |
---|---|---|---|
0 | 0 | 0 | 兼容8048单片机的13位定时器,THn的8位和TLn的5位组成一个13位定时器。 |
0 | 1 | 1 | THn和TLn组成一个16位的定时器。 |
1 | 0 | 2 | 8位自动重装模式,定时器溢出后THn重装到TLn中。 |
1 | 1 | 3 | 禁用定时器1,定时器0变成2个8位定时器。 |
请注意,表5-2的TCON最后标注了“可位寻址”,而表5-4的TMOD标注的是“不可位寻址”。意思就是说:比如TCON有一个位叫TR1,用户可以在程序中直接进行TR1 = 1这样的操作。但对TMOD里的位比如(T1)M1 = 1这样的操作就是错误的。要操作就必须一次操作这整个字节,也就是必须一次性对TMOD所有位操作,不能直接对其中某一位单独进行操作,那么能不能只修改其中的一位而不影响其它位的值呢?当然可以,在后续课程中就会学到方法的。
表5-6列出的就是定时器的4种工作模式,其中模式0是为了兼容老的8048系列单片机而设计的,现在的51几乎不会用到这种模式,而模式3根据应用经验,它的功能用模式2完全可以取代,所以基本上也是不用的,那么重点来学习模式1和模式2。
模式1,是THn和TLn组成了一个16位的定时器,计数范围是0~65535,溢出后,只要不对THn和TLn重新赋值,则从0开始计数。模式2,是8位自动重装载模式,只有TLn做加1计数,计数范围0~255,THn的值并不发生变化,而是保持原值,TLn溢出后,TFn就直接置1了,并且THn原先的值直接赋给TLn,然后TLn从新赋值的这个数字开始计数。这个功能可以用来产生串口的通信波特率,后面章节讲串口的时候要用到。
5.2.3定时器的应用
B站在线视频教程:5-3 定时器实现LED闪烁
了解了定时器相关的寄存器,下面就来做一个定时器的程序,巩固一下学到的内容。这节课的程序先使用定时器0,在使用定时器的时候,需要以下几个步骤:
第一步:设置特殊功能寄存器TMOD,配置好工作模式。
第二步:设置计数寄存器TH0和TL0的初值。
第三步:设置TCON,通过TR0置1来让定时器开始计数。
第四步:判断TCON寄存器的TF0位,监测定时器溢出情况。
写程序之前,要先来学会计算如何用定时器定时时间。Kingst51开发板单片机的晶振是11.0592M,时钟周期就是1/11059200,机器周期是12/11059200,假如要定时20ms,就是0.02秒,要经过x个机器周期得到0.02秒,来算一下x*12/11059200=0.02,得到x= 18432。16位定时器的溢出值是65536(因65535再加1才是溢出),于是就可以这样操作,先给TH0和TL0一个初始值,让它们经过18432个机器周期后刚好达到65536,也就是溢出,溢出后可以通过检测TF0的值得知,就刚好是0.02秒。那么初值y = 65536 - 18432 = 47104,转成16进制就是0xB800,也就是TH0 = 0xB8,TL0 = 0x00。
细心的读者会发现,如果初值直接给一个0x0000,一直到65536溢出,定时器定时值最大也就是71ms左右,那么想定时更长时间怎么办呢?用小学学过的逻辑,倍数关系就可以解决此问题。
下面就用程序来实现LED闪烁功能。
#include <reg52.h>
sbit LED = P0^0;
sbit ADDR0 = P1^0;
sbit ADDR1 = P1^1;
sbit ADDR2 = P1^2;
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;
void main()
{
unsigned char cnt = 0; //定义一个计数变量,记录T0溢出次数
ENLED = 0; //使能U3,选择独立LED
ADDR3 = 1;
ADDR2 = 1;
ADDR1 = 1;
ADDR0 = 0;
TMOD = 0x01; //设置T0为模式1
TH0 = 0xB8; //为T0赋初值0xB800
TL0 = 0x00;
TR0 = 1; //启动T0
while (1)
{
if (TF0 == 1) //判断T0是否溢出
{
TF0 = 0; //T0溢出后,清零中断标志
TH0 = 0xB8; //并重新赋初值
TL0 = 0x00;
cnt++; //计数值自加1
if (cnt >= 50) //判断T0溢出是否达到50次
{
cnt = 0; //达到50次后计数值清零
LED = ~LED; //LED取反:0-->1、1-->0
}
}
}
}
程序中都写了注释,结合前几章学的内容,不难理解。本程序实现的结果是开发板上最右边的小灯点亮一秒,熄灭一秒,也就是以0.5Hz的频率进行闪烁。
5.3数码管的学习
LED小灯是一种简单的LED,只能通过亮和灭来表达简单的信息。这节课学习一种能表达更复杂信息的器件——LED数码管。
5.3.1数码管的基本介绍
B站在线视频教程:5-4 认识数码管
先提供一张数码管原理图,如图5-2所示。
这是比较常见的数码管的原理图,Kingst51开发板子上一共有6个数码管。前边有了LED小灯的学习,数码管学习就会轻松的多了。从图5-3可以看出,数码管共有a、b、c、d、e、f、g、dp这么8个段,而实际上,这8个段每一段都是一个LED小灯,所以一个数码管就是由8个LED小灯组成的。看一下数码管内部结构的示意图,如图5-3。
数码管分为共阳和共阴两种,共阴数码管就是8只LED小灯的阴极是连接在一起的,阴极是公共端,由阳极来控制单个小灯的亮灭。同理,共阳数码管就是阳极接在一起。图5-2的数码管上边有2个com,这就是数码管的公共端。为什么有2个呢,一方面是2个可以起到对称的效果,刚好是10个引脚,另外一个方面,公共端通过的电流较大,初中物理知识有讲,并联电路电流之和等于总电流,用2个com可以把公共电流平均到2个引脚上去,降低单条线路承受的电流。
从开发板的电路图上能看出来,所用的数码管都是共阳数码管,一共有6个,如图5-4所示。
6个数码管的com都是接到了正极上,当然了,和LED小灯电路一样,也是由74HC138控制三极管的导通来控制整个数码管的使能。先来看最右边的DS1这个数码管,原理图上可以看出,控制DS1的三极管是Q17,控制Q17的引脚是LEDS0,对应到74HC138上的就是U3的Y0输出,如图5-5所示。
点亮DS1这个数码管首先要让LEDS0这个引脚输出低电平,相信读者现在可以根据前边学过的知识独立把ADDR0、ADDR1、ADDR2、ADDR3、ENLED这4个所需输入的值写出来了,真正根据74HC138的手册实际操作一次,不需要记住这些结论,但是只要遇到根据手册资料写一次,慢慢遇到同类芯片就知道如何去解决问题了。
数码管通常是用来显示数字的,板子上有6个数码管,习惯上称之为6位,那控制位选择的就是74HC138了。而数码管内部的8个LED小灯称之为数码管的段,数码管的段选择(即该段的亮灭)是通过P0口控制。
5.3.2数码管的真值表
数码管的8个段,直接当成8个LED小灯来控制,那就是a、b、c、d、e、f、g、dp一共8个LED小灯。通过图5-2可以看出,如果点亮b和c这两个LED小灯,也就是数码管的b段和c段,其他的所有的段都熄灭的话,就可以让数码管显示出一个数字1,那么这个时候实际上P0的值就是0b11111001,十六进制就是0xF9。那么写一个程序进去,来看一看数码管显示的效果。
#include <reg52.h>
sbit ADDR0 = P1^0;
sbit ADDR1 = P1^1;
sbit ADDR2 = P1^2;
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;
void main()
{
ENLED = 0; //使能U3,选择数码管DS1
ADDR3 = 1;
ADDR2 = 0;
ADDR1 = 0;
ADDR0 = 0;
P0 = 0xF9; //点亮数码管段b和c
while (1);
}
把这个程序编译一下,并下载到单片机中,就可以看到程序运行的结果是在最右侧的数码管上显示了一个数字1。
用同样的方法,可以把其他的数字字符都在数码管上显示出来,而数码管显示的数字字符对应给P0的赋值,叫做数码管的真值表。来列一下Kingst51开发板电路图的数码管真值表,注意,这个真值表里显示的数字都不带小数点的,如表5-7。
表5-7 数码管真值表
字符 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
数值 | 0xC0 | 0xF9 | 0xA4 | 0xB0 | 0x99 | 0x92 | 0x82 | 0xF8 |
字符 | 8 | 9 | A | B | C | D | E | F |
数值 | 0x80 | 0x90 | 0x88 | 0x83 | 0xC6 | 0xA1 | 0x86 | 0x8E |
可以把上边那个用数码管显示数字1程序中的P0的赋值随便修改成表5-7真值表中的数值,看看显示的数字的效果。
5.3.3数码管的静态显示
B站在线视频教程:5-5 数码管的静态显示
在第3章学习了74HC138,了解到74HC138在同一时刻只能让一个输出口为低电平,也就是说在一个时刻内,只能使能一个数码管,并根据给出的P0的值来改变这个数码管的显示字符,可以将此理解为数码管的静态显示。 数码管静态显示是对应动态显示而言,静态显示对于一两个数码管还行,多个数码管,静态显示实现的意义就没有了。这节课先用一个数码管的静态显示来实现简单的秒表,为下节课的动态显示打下基础。
51单片机的C语言编程有一个关键字code。前边课程定义变量的时候,一般用到unsigned char或者unsigned int这两个关键字,这样定义的变量都是放在单片机的RAM中,程序中可以随意去改变这些变量的值。但是还有一种数据,在程序中要使用,但是却不会改变它的值,定义这种数据时可以加一个code关键字修饰一下,这个数据就会存储到单片机的程序空间Flash中,这样可以大大节省单片机的RAM的使用量,毕竟单片机RAM空间比较小,而程序空间则大的多。那么现在要使用的数码管真值表,只会使用它们的值,而不需要改变它们,就可以用code关键字把它放入Flash中了,具体程序代码如下。
#include <reg52.h>
sbit ADDR0 = P1^0;
sbit ADDR1 = P1^1;
sbit ADDR2 = P1^2;
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;
//用数组来存储数码管的真值表,数组将在下一章详细介绍
unsigned char code LedChar[] = {
0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,
0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E
};
void main()
{
unsigned char cnt = 0; //记录T0中断次数
unsigned char sec = 0; //记录经过的秒数
ENLED = 0; //使能U3,选择数码管DS1
ADDR3 = 1;
ADDR2 = 0;
ADDR1 = 0;
ADDR0 = 0;
TMOD = 0x01; //设置T0为模式1
TH0 = 0xB8; //为T0赋初值0xB800
TL0 = 0x00;
TR0 = 1; //启动T0
while (1)
{
if (TF0 == 1) //判断T0是否溢出
{
TF0 = 0; //T0溢出后,清零中断标志
TH0 = 0xB8; //并重新赋初值
TL0 = 0x00;
cnt++; //计数值自加1
if (cnt >= 50) //判断T0溢出是否达到50次
{
cnt = 0; //达到50次后计数值清零
P0 = LedChar[sec]; //当前秒数对应的真值表中的值送到P0口
sec++; //秒数记录自加1
if (sec >= 16) //当秒数超过0x0F(15)后,重新从0开始
{
sec = 0;
}
}
}
}
}
5.4练习题
- 熟练掌握单片机定时器的原理和应用方法。
- 使用定时器来实现左右移动的流水灯程序。
- 了解数码管的原理,掌握数码管的真值表的计算方法。
- 编程实现数码管静态显示秒表的倒计时。